#ifndef __RENDERER_HPP__
#define __RENDERER_HPP__ 1

#include <cassert>
#include <algorithm>
#include <vector>
#include <thread>
#include <mutex>
#include <atomic>

#include <Windows.h>
#include <Wingdi.h>

#include <GL/glew.h>
#include <GL/wglew.h>

#include <FreeImagePlus.h>

// forwards
enum class EventType;
struct Event;
class EventHandler;
class CWindow;
class CScene;

bool CheckForErrors();

// TODO
struct Uniform
{
};

struct TextureParameter
{
	TextureParameter()
	{

	}
	~TextureParameter()
	{
	}

	GLenum target;
	GLenum pname;
	GLint param;
};

// TODO support mipmaps
struct Texture
{
	Texture(GLsizei width, GLsizei height, GLsizei depth, GLenum target, GLint lodLevel, GLint format, GLint border, GLenum pixelFormat, GLenum pixelType, const GLvoid* data) : 
		width(width), height(height), depth(depth), target(target), lodLevel(lodLevel), format(format), border(border), pixelFormat(pixelFormat), pixelType(pixelType), data(data)
	{
		assert(width % 2 == 0 && height % 2 == 0 && depth % 2 == 0);

		::glPixelStorei(GL_UNPACK_ALIGNMENT, 4);

		::glGenTextures(1, &textureId);
		::glActiveTexture(GL_TEXTURE0);
		::glBindTexture(target, textureId);

		switch(target)
		{
		case GL_TEXTURE_1D:
		case GL_PROXY_TEXTURE_1D:
			::glTexImage1D(target, lodLevel, format, width, border, pixelFormat, pixelType, data);
			break;

		case GL_TEXTURE_2D:
		case GL_PROXY_TEXTURE_2D:
		case GL_TEXTURE_1D_ARRAY:
		case GL_PROXY_TEXTURE_1D_ARRAY:
		case GL_TEXTURE_RECTANGLE:
		case GL_PROXY_TEXTURE_RECTANGLE:
		case GL_TEXTURE_CUBE_MAP_POSITIVE_X:
		case GL_TEXTURE_CUBE_MAP_NEGATIVE_X:
		case GL_TEXTURE_CUBE_MAP_POSITIVE_Y:
		case GL_TEXTURE_CUBE_MAP_NEGATIVE_Y:
		case GL_TEXTURE_CUBE_MAP_POSITIVE_Z:
		case GL_TEXTURE_CUBE_MAP_NEGATIVE_Z:
		case GL_PROXY_TEXTURE_CUBE_MAP:
			::glTexImage2D(target, lodLevel, format, width, height, border, pixelFormat, pixelType, data);
			break;

		case GL_TEXTURE_3D:
		case GL_PROXY_TEXTURE_3D:
		case GL_TEXTURE_2D_ARRAY:
		case GL_PROXY_TEXTURE_2D_ARRAY:
			::glTexImage3D(target, lodLevel, format, width, height, depth, border, pixelFormat, pixelType, data);
			break;
		}

		::glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

		CheckForErrors();
	}
	~Texture()
	{
	}

	GLuint textureId;

	GLsizei width;
	GLsizei height;
	GLsizei depth;

	GLenum target;
	GLint lodLevel;
	GLint format;
	GLint border;
	GLenum pixelFormat;
	GLenum pixelType;
	const GLvoid* data;

	std::vector<fipImage*> images;
	std::vector<TextureParameter*> textureParams;
};

struct VertexAttributes
{
	VertexAttributes(GLuint idx, GLint components, GLenum type, GLboolean normalized, GLsizei stride, GLvoid* ptr) :
			idx(idx), components(components), type(type), normalized(normalized), stride(stride), ptr(ptr)
	{
	}

	~VertexAttributes()
	{
		::glDisableVertexAttribArray(idx);
	}

	VertexAttributes(VertexAttributes&& other)
	{
		*this = std::move(other);
	}

	VertexAttributes& operator=(VertexAttributes&& other)
	{
		std::swap(this->components, other.components);
		std::swap(this->idx, other.idx);
		std::swap(this->normalized, other.normalized);
		std::swap(this->ptr, other.ptr);
		std::swap(this->stride, other.stride);
		std::swap(this->type, other.type);

		return *this;
	}

	bool Enable()
	{
		::glEnableVertexAttribArray(idx);

		return CheckForErrors();
	}

	bool Disable()
	{
		::glDisableVertexAttribArray(idx);

		return CheckForErrors();
	}

	bool AttachAttributes()
	{
		::glEnableVertexAttribArray(idx);
		CheckForErrors();

		switch(type)
		{
		case GL_UNSIGNED_BYTE:
		case GL_BYTE:
		case GL_UNSIGNED_SHORT:
		case GL_SHORT:
		case GL_UNSIGNED_INT:
		case GL_INT:
			if(!normalized)
				::glVertexAttribIPointer(idx, components, type, stride, ptr);
			else
				::glVertexAttribPointer(idx, components, type, normalized, stride, ptr);
			break;
		case GL_FLOAT:
			::glVertexAttribPointer(idx, components, type, normalized, stride, ptr);
			break;
		case GL_DOUBLE:
			::glVertexAttribLPointer(idx, components, type, stride, ptr);
			break;
		default:
			return false;
		}

		return CheckForErrors();
	}

	GLuint idx;
	GLint components;
	GLenum type;
	GLboolean normalized;
	GLsizei stride;
	GLvoid* ptr;
};

// Refactor to a general buffer class or use inheritence to support multiple buffer types
struct VertexBuffer
{
	template<typename T>
	VertexBuffer(GLenum target, GLenum usage, GLsizei count, std::vector<std::vector<T>>& buffers, std::vector<VertexAttributes*> vertexAttributes) :
			target(target), usage(usage), count(count), vertexAttributes(vertexAttributes)
	{
		assert(count && "Count is zero");
		bufferId = new GLuint[count];

		::glGenBuffers(count, bufferId);

		CheckForErrors();

		if(!GenerateBuffers<T>(buffers))
			throw std::exception("Could not generate buffers");		
	}
	
	template<typename T>
	VertexBuffer(GLenum target, GLenum usage, GLsizei count, std::vector<std::vector<T>>& buffers) :
			target(target), usage(usage), count(count)
	{
		assert(count && "Count is zero");
		bufferId = new GLuint[count];

		::glGenBuffers(count, bufferId);

		CheckForErrors();

		if(!GenerateBuffers<T>(buffers))
			throw std::exception("Could not generate buffers");		
	}

	~VertexBuffer()
	{
		::glBindBuffer(target, 0);

		// Clean up buffer attributes
		for(auto itVA(vertexAttributes.begin()), endVA(vertexAttributes.end()); itVA != endVA; ++itVA)
			delete *itVA;

		// check if glDelete* functions are still blocking in OpenGL >=3.3 core
		::glDeleteBuffers(count, bufferId);

		delete[] bufferId;
	}

	VertexBuffer(VertexBuffer&& other) : vertexAttributes(std::move(other.vertexAttributes))
	{
	}

	VertexBuffer& operator=(VertexBuffer&& other)
	{
		std::swap(this->count, other.count);
		std::swap(this->bufferId, other.bufferId);
		std::swap(this->target, other.target);
		std::swap(this->usage, other.usage);
		std::swap(this->vertexAttributes, other.vertexAttributes);

		return *this;
	}

	void Bind(GLuint idx)
	{
		assert(idx >= 0 && idx < (GLuint)count);
		::glBindBuffer(target, bufferId[idx]);
	}

	void Unbind()
	{
		::glBindBuffer(target, 0);
	}

	template<typename T>
	bool GenerateBuffers(std::vector<std::vector<T>>& buffers)
	{
		for(GLsizei i = 0; i < count; ++i)
		{
			::glBindBuffer(target, bufferId[i]);
			if(!CheckForErrors())
				return false;

			size_t size = sizeof(std::vector<T>::value_type) * buffers[i].size();
			::glBufferData(target, size, buffers[i].data(), usage);
			if(!CheckForErrors())
				return false;

			::glBindBuffer(target, 0);

			if(!CheckForErrors())
				return false;
		}
		return true;
	}

	void SetAttributes(GLuint idx)
	{
		assert(idx >= 0 && idx < (GLuint)count);

		// Set attributes
		vertexAttributes[idx]->AttachAttributes();
	}

	GLenum target;
	GLenum usage;
	GLsizei count;
	GLuint* bufferId;

	std::vector<VertexAttributes*> vertexAttributes;
};

struct VertexArray
{
	VertexArray(GLsizei count, std::vector<VertexAttributes*> vertexAttributes) : count(count), vertexAttributes(vertexAttributes)
	{
		assert(count > 0);
		arrayId = new GLuint[count];
		::glGenVertexArrays(count, arrayId);

		CheckForErrors();
	}

	VertexArray(GLsizei count) : count(count)
	{
		assert(count > 0);
		arrayId = new GLuint[count];
		::glGenVertexArrays(count, arrayId);

		CheckForErrors();
	}

	~VertexArray()
	{
		// Clean up buffer attributes
		for(auto itVA(vertexAttributes.begin()), endVA(vertexAttributes.end()); itVA != endVA; ++itVA)
			delete *itVA;

		for(auto itVB(vertexBuffers.begin()), endVB(vertexBuffers.end()); itVB != endVB; ++itVB)
			delete *itVB;

		::glBindVertexArray(0);
		::glDeleteVertexArrays(count, arrayId);

		delete[] arrayId;
	}

	VertexArray(VertexArray&& other) : 
			vertexAttributes(std::move(other.vertexAttributes)), vertexBuffers(std::move(other.vertexBuffers))
	{
	}

	VertexArray& operator=(VertexArray&& other)
	{
		std::swap(this->count, other.count);
		std::swap(this->arrayId, other.arrayId);
		std::swap(this->vertexAttributes, other.vertexAttributes);
		std::swap(this->vertexBuffers, other.vertexBuffers);

		return *this;
	}

	void Bind(GLuint idx)
	{
		assert(idx >= 0 && idx < (GLuint)count);
		::glBindVertexArray(arrayId[idx]);
	}

	void Unbind()
	{
		::glBindVertexArray(0);
	}

	void SetAttributes(GLuint idx)
	{
		assert(idx >= 0 && idx < (GLuint)vertexAttributes.size());
		vertexAttributes[idx]->AttachAttributes();
	}
	
	GLsizei count;
	GLsizei attr_count;
	GLuint* arrayId;

	std::vector<VertexBuffer*> vertexBuffers;
	std::vector<VertexAttributes*> vertexAttributes;
};

struct RenderData
{
	RenderData();
	~RenderData();

	HGLRC hGLRC;
	CWindow* window;
	CScene* scene;
};

class CRenderer
{
public:

	enum class RenderState : int32_t
	{
		Initializing = 0,
		AddRenderTargets,
		RemoveRenderTargets,
		Render,
		Shutdown
	};

	CRenderer(int glVerMajor, int glVerMinor, EventHandler& eventHandler);
	~CRenderer();

	const CRenderer& operator=(const CRenderer& other) { return other; }

	int RenderThread();

	bool Run();

	void SetState(RenderState state) { this->state = state; }
	RenderState GetState() { return state; }

	bool InitializePixelFormat(CWindow* window);
	RenderData* CreateRenderContext(int glVerMajor, int glVerMinor, CWindow* window);
	bool Render(CScene* scene);

	std::unique_lock<std::mutex> GetLock() { return std::unique_lock<std::mutex>(mtx, std::defer_lock); }

	std::vector<CWindow*>& GetNewWindowTargets() { return newTargetWindows; }
	std::vector<RenderData*>& GetRenderTargets() { return renderTargets; }

	// Event handlers
	bool OnWindowCreated(Event* evt);
	bool OnWindowDestroyed(Event* evt);
	//bool OnWindowResize(Event* evt);

private:
	CRenderer(const CRenderer&);
	//CRenderer& operator=(const CRenderer&);

	EventHandler& eventHandler;

	std::thread thr;
	std::mutex mtx;
	RenderState state;

	std::vector<CWindow*> newTargetWindows;
	std::vector<CWindow*> oldTargetWindows;
	std::vector<RenderData*> renderTargets;

	GLint glVersion[2];
	std::vector<GLuint> buffers;
};

#endif
